package eu.rekawek.coffeegb.controller.network

import eu.rekawek.coffeegb.controller.Input
import eu.rekawek.coffeegb.controller.events.register
import eu.rekawek.coffeegb.controller.link.LinkedController
import eu.rekawek.coffeegb.core.Gameboy.BootstrapMode
import eu.rekawek.coffeegb.core.GameboyType
import eu.rekawek.coffeegb.core.events.Event
import eu.rekawek.coffeegb.core.events.EventBus
import eu.rekawek.coffeegb.core.joypad.Button
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.ByteBuffer
import kotlin.concurrent.Volatile
import org.slf4j.Logger
import org.slf4j.LoggerFactory

class Connection(
    private val inputStream: InputStream,
    private val outputStream: OutputStream,
    mainEventBus: EventBus,
    private val server: Boolean,
) : Runnable, AutoCloseable {

  private val eventBus: EventBus = mainEventBus.fork("connection")

  @Volatile private var doStop = false

  init {
    eventBus.register<LinkedController.LocalRomLoadedEvent> {
      outputStream.write(0x01)

      val buf = ByteBuffer.allocate(22)
      buf.putLong(it.frame)
      buf.put(it.gameboyType.ordinal.toByte())
      buf.put(it.bootstrapMode.ordinal.toByte())
      buf.putInt(it.romFile.size)
      buf.putInt(it.batteryFile?.size ?: 0)
      buf.putInt(it.snapshot?.size ?: 0)
      buf.rewind()

      outputStream.write(buf.array())
      outputStream.write(it.romFile)
      if (it.batteryFile != null) {
        outputStream.write(it.batteryFile)
      }
      if (it.snapshot != null) {
        outputStream.write(it.snapshot)
      }
    }
    eventBus.register<LinkedController.LocalButtonStateEvent> {
      outputStream.write(0x03)
      val buf = ByteBuffer.allocate(10)
      buf.putLong(it.frame)
      buf.put(it.input.pressedButtons.size.toByte())
      buf.put(it.input.releasedButtons.size.toByte())
      outputStream.write(buf.array())

      for (button in it.input.pressedButtons) {
        outputStream.write(button.ordinal)
      }
      for (button in it.input.releasedButtons) {
        outputStream.write(button.ordinal)
      }
      outputStream.flush()
      LOG.atDebug().log("Sent {}", it)
    }
    eventBus.register<RequestResetEvent> {
      outputStream.write(0x06)

      val buf = ByteBuffer.allocate(8)
      buf.putLong(it.frame)
      outputStream.write(buf.array())

      outputStream.flush()
      LOG.atInfo().log("Sent {}", it)
    }
    eventBus.register<RequestStopEvent> {
      outputStream.write(0x07)

      val buf = ByteBuffer.allocate(8)
      buf.putLong(it.frame)
      outputStream.write(buf.array())

      outputStream.flush()
      LOG.atInfo().log("Sent {}", it)
    }
  }

  override fun run() {
    handshake()
    while (!doStop) {
      val command = inputStream.read()
      if (command == -1) {
        return
      }
      val event: Event? =
          when (command) {
            // peer loaded game
            0x01 -> {
              val buf = ByteBuffer.allocate(22)
              inputStream.read(buf.array())

              val frame = buf.getLong()
              val gameboyType = GameboyType.entries[buf.get().toInt()]
              val bootstrapMode = BootstrapMode.entries[buf.get().toInt()]
              val romSize = buf.getInt()
              val batterySize = buf.getInt()
              val snapshotSize = buf.getInt()

              val rom = inputStream.readNBytes(romSize)
              val battery =
                  if (batterySize > 0) {
                    inputStream.readNBytes(batterySize)
                  } else {
                    null
                  }
              val snapshot =
                  if (snapshotSize > 0) {
                    inputStream.readNBytes(snapshotSize)
                  } else {
                    null
                  }

              PeerLoadedGameEvent(rom, battery, snapshot, gameboyType, bootstrapMode, frame)
            }
            // sync
            0x03 -> {
              val buf = ByteBuffer.allocate(10)
              inputStream.read(buf.array())

              val frame = buf.getLong()
              val pressedCount = buf.get()
              val releasedCount = buf.get()
              val pressed = readButtons(pressedCount.toInt())
              val released = readButtons(releasedCount.toInt())
              LinkedController.RemoteButtonStateEvent(frame, Input(pressed, released))
            }

            // reset
            0x06 -> {
              val buf = ByteBuffer.allocate(8)
              inputStream.read(buf.array())
              val frame = buf.getLong()

              ReceivedRemoteResetEvent(frame)
            }

            // stop
            0x07 -> {
              val buf = ByteBuffer.allocate(8)
              inputStream.read(buf.array())
              val frame = buf.getLong()

              ReceivedRemoteStopEvent(frame)
            }

            else -> {
              LOG.atWarn().log("Received remote unknown command $command")
              null
            }
          }
      if (event != null) {
        when (event) {
          is PeerLoadedGameEvent ->
              LOG.atInfo()
                  .log("Received remote command $command, posting event: ${event.javaClass}")
          is LinkedController.RemoteButtonStateEvent ->
              LOG.atDebug().log("Received remote command $command, posting event: $event")
          else -> LOG.atInfo().log("Received remote command $command, posting event: $event")
        }

        eventBus.post(event)
      }
    }
  }

  private fun readButtons(count: Int): List<Button> {
    return (0 until count).map {
      val buttonId = inputStream.read()
      Button.entries[buttonId]
    }
  }

  fun stop() {
    doStop = true
  }

  override fun close() {
    eventBus.close()
    inputStream.close()
    outputStream.close()
  }

  private fun handshake() {
    val buf = ByteBuffer.allocate(PROTOCOL_NAME.length + 1)
    if (server) {
      buf.put(PROTOCOL_NAME.toByteArray())
      buf.put(PROTOCOL_VERSION)
      outputStream.write(buf.array())
      LOG.atInfo().log("Sent protocol name {} and version {}", PROTOCOL_NAME, PROTOCOL_VERSION)
    } else {
      inputStream.read(buf.array())
      val receivedProtocolName = String(buf.array().take(PROTOCOL_NAME.length).toByteArray())
      if (receivedProtocolName != PROTOCOL_NAME) {
        throw IOException(
            "Protocol mismatch: expected $PROTOCOL_NAME, received $receivedProtocolName"
        )
      }
      val receivedProtocolVersion = buf.array()[PROTOCOL_NAME.length]
      if (receivedProtocolVersion != PROTOCOL_VERSION) {
        throw IOException(
            "Protocol mismatch: expected $PROTOCOL_VERSION, received $receivedProtocolVersion"
        )
      }
      LOG.atInfo()
          .log(
              "Received protocol name {} and version {}",
              receivedProtocolName,
              receivedProtocolVersion,
          )
    }
  }

  data class PeerLoadedGameEvent(
      val rom: ByteArray,
      val battery: ByteArray?,
      val snapshot: ByteArray?,
      val gameboyType: GameboyType,
      val bootstrapMode: BootstrapMode,
      val frame: Long,
  ) : Event

  data class RequestResetEvent(val frame: Long) : Event

  data class RequestStopEvent(val frame: Long) : Event

  data class ReceivedRemoteResetEvent(val frame: Long) : Event

  data class ReceivedRemoteStopEvent(val frame: Long) : Event

  companion object {
    private val LOG: Logger = LoggerFactory.getLogger(Connection::class.java)
    private const val PROTOCOL_NAME: String = "CoffeeGB NETPLAY"
    private const val PROTOCOL_VERSION: Byte = 0x01
  }
}
